package edu.pw.p2pclient.logic;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

import android.annotation.SuppressLint;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.util.Log;
import edu.pw.p2pclient.data.DownloadAdapter;
import edu.pw.p2pclient.data.DownloadNode;
import edu.pw.p2pclient.data.RemoteDeviceInfo;
import edu.pw.p2pclient.networking.CommunicationService;
import edu.pw.p2pclient.networking.DownloadTask;
import edu.pw.p2pclient.utils.ApplicationContext;

public class DownloadManager {

	public final static int MAX_DOWNLOADS = 2;
	public final static int MAX_SINGLE_DOWNLOADS = 5;

	// Handler messages
	public final static int UPDATE_LIST_VIEW = 0;
	public final static int START_DOWNLOAD = 1;

	private final static String TAG = "DownloadManager";
	private final static String DATA_FILE = "/download.dat";

	private static DownloadAdapter mDownloadAdapter;
	private static PushDownloadAdapter mPushDownloadAdapter;

	private static DownloadManager mInstance = null;
	private HashMap<DownloadNode, ArrayList<DownloadTask>> mCurrent = new HashMap<DownloadNode, ArrayList<DownloadTask>>();
	private int mCurrentlyDownloading = 0;

	private static Handler mHandler = new Handler() {

		public void handleMessage(android.os.Message msg) {
			switch (msg.what) {
			case UPDATE_LIST_VIEW:
				mDownloadAdapter.notifyDataSetChanged();
				mPushDownloadAdapter.notifyDataSetChanged();
				break;
			case START_DOWNLOAD:
				if (mInstance != null)
					mInstance.startDownloads();
				break;
			}
		};
	};

	@SuppressWarnings("unchecked")
	private DownloadManager() {
		mDownloadAdapter = new DownloadAdapter();
		mPushDownloadAdapter = new PushDownloadAdapter();
		ArrayList<DownloadNode> downloading = null;
		ArrayList<DownloadNode> pushDownloading = null;
		ObjectInputStream ois = null;
		try {
			FileInputStream fin = new FileInputStream(ApplicationContext.get()
					.getFilesDir().getParent()
					+ DATA_FILE);
			ois = new ObjectInputStream(fin);
			try {
				downloading = (ArrayList<DownloadNode>) ois.readObject();
			} catch (Exception ex) {
				downloading = null;
				return;
			}
			try {
				pushDownloading = (ArrayList<DownloadNode>) ois.readObject();
			} catch (Exception ex) {
				pushDownloading = null;
			}
		} catch (FileNotFoundException ex) {
			downloading = null;
			pushDownloading = null;
		} catch (Exception ex) {
			downloading = null;
			pushDownloading = null;
		} finally {
			try {
				if (ois != null)
					ois.close();
			} catch (IOException e) {
			}
		}
		if (downloading != null) {
			for (DownloadNode node : downloading)
				mDownloadAdapter.add(node);
		}
		if (pushDownloading != null) {
			for (DownloadNode node : pushDownloading)
				mPushDownloadAdapter.add(node);
		}
	}

	public static DownloadManager getInstance() {

		if (mInstance == null)
			mInstance = new DownloadManager();
		return mInstance;
	}

	public DownloadAdapter getDownloadAdapter() {
		return mDownloadAdapter;
	}

	public PushDownloadAdapter getPushDownloadAdapter() {
		return mPushDownloadAdapter;
	}

	public void updateDownloadList() {
		mHandler.sendEmptyMessage(UPDATE_LIST_VIEW);
	}

	private void add(DownloadNode node, int part) {

		DownloadTask task = new DownloadTask(node, part);
		ArrayList<DownloadTask> tmp = mCurrent.get(node);
		if (tmp == null) {
			tmp = new ArrayList<DownloadTask>();
			tmp.add(task);
			mCurrent.put(node, tmp);
			++mCurrentlyDownloading;
		} else
			tmp.add(task);
		task.run();
	}

	private void remove(DownloadNode node, int part) {

		if (mCurrent.get(node) == null)
			return;
		Iterator<DownloadTask> tmp = mCurrent.get(node).iterator();
		while (tmp.hasNext()) {
			DownloadTask task = tmp.next();
			if (task.getPart() == part)
				tmp.remove();
		}
		if (mCurrent.get(node).size() == 0) {
			mCurrent.remove(node);
			--mCurrentlyDownloading;
		}
	}

	private boolean canAdd(DownloadNode node) {
		if (CommunicationService.getInstance() == null
				|| !CommunicationService.isRunning())
			return false;

		ArrayList<DownloadTask> tmp = mCurrent.get(node);
		return tmp == null ? mCurrentlyDownloading < MAX_DOWNLOADS
				: tmp.size() < MAX_SINGLE_DOWNLOADS;
	}

	// Function called when user adds new resource to downloads
	public void scheduleDownload(DownloadNode node) {

		node.setState(DownloadNode.QUEUED);
		mDownloadAdapter.add(node);
		startDownload(node);
	}

	public void startDownloads() {
		for (int i = 0; i < mPushDownloadAdapter.getCount()
				&& mCurrentlyDownloading < MAX_DOWNLOADS; ++i)
			startDownload(mPushDownloadAdapter.getItem(i));
		for (int i = 0; i < mDownloadAdapter.getCount()
				&& mCurrentlyDownloading < MAX_DOWNLOADS; ++i)
			startDownload(mDownloadAdapter.getItem(i));
	}

	public void startDownload(DownloadNode node) {

		while (node != null && (node.isDownloading() || node.isQueued())
				&& node.getAmountOfMissingParts() > 0 && canAdd(node)) {
			node.setState(DownloadNode.DOWNLOADING);
			ArrayList<Integer> missing = node.getMissingParts();
			synchronized (missing) {
				if (missing.size() > 0) {
					int part = missing.get(0);
					add(node, part);
					node.partDownloadStarted(part);
				}
			}
		}
		updateDownloadList();
		saveDownloadingList();
	}

	public void onPartDownloadFinished(DownloadNode node, int part, byte[] data) {

		Log.i(TAG,
				"Download of part " + part + " of resource " + node.getName()
						+ " finished.");
		remove(node, part);
		if (node.getState() != DownloadNode.CANCELLED) {
			node.partDownloaded(part, data);
			if (node.isDone())
				downloadNext(node);
			else
				startDownload(node);
		} else
			downloadNext(node);
	}

	public void onPartdownloadFailed(DownloadNode node, int part,
			boolean skipResource) {

		remove(node, part);
		node.partFailed(part);
		if (skipResource)
			downloadNext(node);
		else
			startDownload(node);
	}

	public void downloadNext(DownloadNode curNode) {

		DownloadNode node = null;
		if (mPushDownloadAdapter.getPosition(curNode) != -1) {
			for (int i = mPushDownloadAdapter.getPosition(curNode) + 1; mCurrentlyDownloading < MAX_DOWNLOADS
					&& i < mPushDownloadAdapter.getCount(); ++i) {
				node = mPushDownloadAdapter.getItem(i);
				if (node.isQueued()
						&& (!node.getSharingInfo().isPasswordProtected() || !node
								.getSharingInfo().getPassword().equals("")))
					startDownload(node);
			}
		}
		for (int i = mDownloadAdapter.getPosition(curNode) + 1; mCurrentlyDownloading < MAX_DOWNLOADS
				&& i < mDownloadAdapter.getCount(); ++i) {
			node = mDownloadAdapter.getItem(i);
			if (node.isQueued()
					&& (!node.getSharingInfo().isPasswordProtected() || !node
							.getSharingInfo().getPassword().equals("")))
				startDownload(node);
		}
		updateDownloadList();
		saveDownloadingList();
	}

	public void cancelDownload(DownloadNode node) {

		node.setState(DownloadNode.CANCELLED);
		downloadNext(node);
	}

	public void saveDownloadingList() {

		new AsyncTask<Void, Void, Void>() {

			@Override
			protected Void doInBackground(Void... params) {
				ArrayList<DownloadNode> downloading = new ArrayList<DownloadNode>(
						mDownloadAdapter.getCount());
				for (int i = 0; i < mDownloadAdapter.getCount(); ++i)
					downloading.add(mDownloadAdapter.getItem(i));
				ArrayList<DownloadNode> pushDownloading = new ArrayList<DownloadNode>(
						mPushDownloadAdapter.getCount());
				for (int i = 0; i < mPushDownloadAdapter.getCount(); ++i)
					pushDownloading.add(mPushDownloadAdapter.getItem(i));
				try {
					FileOutputStream fout = new FileOutputStream(
							ApplicationContext.get().getFilesDir().getParent()
									+ DATA_FILE);
					ObjectOutputStream oos = new ObjectOutputStream(fout);
					oos.writeObject(downloading);
					oos.writeObject(pushDownloading);
					oos.close();

				} catch (Exception ex) {
					ex.printStackTrace();
				}
				return null;
			}

			@SuppressLint("NewApi")
			public void run(Void... params) {
				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
					this.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
							params);
				} else {
					this.execute(params);
				}
			}
		}.run();
	}

	public void onNewResourceListVersionObtained(RemoteDeviceInfo device) {
		mPushDownloadAdapter.addAllPush(device.getResources());
		mHandler.sendEmptyMessage(START_DOWNLOAD);
	}
}
